// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default

#include "qwebenginedownloadrequest.h"
#include "qwebenginedownloadrequest_p.h"

#include "qwebenginepage.h"

#include "profile_adapter.h"
#include "web_contents_adapter_client.h"

#include <QDir>
#include <QFileInfo>

QT_BEGIN_NAMESPACE

using QtWebEngineCore::ProfileAdapterClient;

ASSERT_ENUMS_MATCH(ProfileAdapterClient::NoReason, QWebEngineDownloadRequest::NoReason)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileFailed, QWebEngineDownloadRequest::FileFailed)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileAccessDenied, QWebEngineDownloadRequest::FileAccessDenied)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileNoSpace, QWebEngineDownloadRequest::FileNoSpace)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileNameTooLong, QWebEngineDownloadRequest::FileNameTooLong)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileTooLarge, QWebEngineDownloadRequest::FileTooLarge)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileVirusInfected, QWebEngineDownloadRequest::FileVirusInfected)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileTransientError, QWebEngineDownloadRequest::FileTransientError)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileBlocked, QWebEngineDownloadRequest::FileBlocked)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileSecurityCheckFailed, QWebEngineDownloadRequest::FileSecurityCheckFailed)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileTooShort, QWebEngineDownloadRequest::FileTooShort)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::FileHashMismatch, QWebEngineDownloadRequest::FileHashMismatch)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::NetworkFailed, QWebEngineDownloadRequest::NetworkFailed)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::NetworkTimeout, QWebEngineDownloadRequest::NetworkTimeout)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::NetworkDisconnected, QWebEngineDownloadRequest::NetworkDisconnected)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::NetworkServerDown, QWebEngineDownloadRequest::NetworkServerDown)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::NetworkInvalidRequest, QWebEngineDownloadRequest::NetworkInvalidRequest)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerFailed, QWebEngineDownloadRequest::ServerFailed)
//ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerNoRange, QWebEngineDownloadRequest::ServerNoRange)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerBadContent, QWebEngineDownloadRequest::ServerBadContent)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerUnauthorized, QWebEngineDownloadRequest::ServerUnauthorized)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerCertProblem, QWebEngineDownloadRequest::ServerCertProblem)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerForbidden, QWebEngineDownloadRequest::ServerForbidden)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::ServerUnreachable, QWebEngineDownloadRequest::ServerUnreachable)
ASSERT_ENUMS_MATCH(ProfileAdapterClient::UserCanceled, QWebEngineDownloadRequest::UserCanceled)
//ASSERT_ENUMS_MATCH(ProfileAdapterClient::UserShutdown, QWebEngineDownloadRequest::UserShutdown)
//ASSERT_ENUMS_MATCH(ProfileAdapterClient::Crash, QWebEngineDownloadRequest::Crash)

static inline QWebEngineDownloadRequest::DownloadState toDownloadState(int state)
{
    switch (state) {
    case ProfileAdapterClient::DownloadInProgress:
        return QWebEngineDownloadRequest::DownloadInProgress;
    case ProfileAdapterClient::DownloadCompleted:
        return QWebEngineDownloadRequest::DownloadCompleted;
    case ProfileAdapterClient::DownloadCancelled:
        return QWebEngineDownloadRequest::DownloadCancelled;
    case ProfileAdapterClient::DownloadInterrupted:
        return QWebEngineDownloadRequest::DownloadInterrupted;
    default:
        Q_UNREACHABLE_RETURN(QWebEngineDownloadRequest::DownloadCancelled);
    }
}

static inline QWebEngineDownloadRequest::DownloadInterruptReason toDownloadInterruptReason(int reason)
{
    return static_cast<QWebEngineDownloadRequest::DownloadInterruptReason>(reason);
}

/*!
    \class QWebEngineDownloadRequest
    \brief The QWebEngineDownloadRequest class provides information about a download.

    \inmodule QtWebEngineCore

    QWebEngineDownloadRequest models a download throughout its life cycle, starting
    with a pending download request and finishing with a completed download. It
    can be used, for example, to get information about new downloads, to monitor
    progress, and to pause, resume, and cancel downloads.

    Downloads are usually triggered by user interaction on a web page. It is the
    QWebEngineProfile's responsibility to notify the application of new download
    requests, which it does by emitting the
    \l{QWebEngineProfile::downloadRequested}{downloadRequested} signal together
    with a newly created QWebEngineDownloadRequest. The application can then
    examine this item and decide whether to accept it or not. When a decision is
    made, the application must explicitly call accept() or cancel() on the item
    for \QWE to actually start downloading or rejecting the request.

    \note Some properties, such as setting the path and file name where the file
    will be saved (see \l downloadDirectory() and \l downloadFileName()), can
    only be changed before calling accept().

    \section2 Object Life Cycle

    In each and every case, the QWebEngineProfile takes the ownership of the item.
    However, it is safe for the application to delete the item at any time, except
    during the handling of the \l{QWebEngineProfile::downloadRequested}{downloadRequested}
    signal. The QWebEngineProfile being a long-lived object, it is in fact recommended that
    the application delete any items it is no longer interested in.

    \note Deleting an item will also automatically cancel a download since 5.12.2,
    but it is recommended to cancel manually before deleting for portability.

    \section2 Web Page Downloads

    In addition to normal file downloads, which consist simply of retrieving
    some raw bytes from the network and writing them to disk, \QWE also
    supports saving complete web pages, which involves parsing the page's HTML,
    downloading any dependent resources, and potentially packaging everything
    into a special file format (\l savePageFormat). To check if a download is
    for a file or a web page, use \l isSavePageDownload.

    Web page save requests are accepted automatically and started from
    \l DownloadInProgress state by convenience reasons. The first directly connected
    \l{QWebEngineProfile::downloadRequested}{downloadRequested} signal handler
    can prevent this by calling cancel(), otherwise the save operation will start
    writing data to the disk.

    \sa QWebEngineProfile, QWebEngineProfile::downloadRequested,
    QWebEnginePage::download, QWebEnginePage::save
*/

QWebEngineDownloadRequestPrivate::QWebEngineDownloadRequestPrivate(
        QtWebEngineCore::ProfileAdapter *adapter)
    : profileAdapter(adapter)
{
}

QWebEngineDownloadRequestPrivate::~QWebEngineDownloadRequestPrivate()
{
}

void QWebEngineDownloadRequestPrivate::update(const ProfileAdapterClient::DownloadItemInfo &info)
{
    Q_Q(QWebEngineDownloadRequest);

    // Don't change download state until users accept/cancel the request
    if (!answered && downloadState == QWebEngineDownloadRequest::DownloadRequested)
        return;

    if (toDownloadInterruptReason(info.downloadInterruptReason) != interruptReason) {
        interruptReason = toDownloadInterruptReason(info.downloadInterruptReason);
        Q_EMIT q->interruptReasonChanged();
    }
    if (toDownloadState(info.state) != downloadState) {
        downloadState = toDownloadState(info.state);
        Q_EMIT q->stateChanged(downloadState);
    }
    if (info.receivedBytes != receivedBytes) {
        receivedBytes = info.receivedBytes;
        Q_EMIT q->receivedBytesChanged();
    }
    if (info.totalBytes != totalBytes) {
        totalBytes = info.totalBytes;
        Q_EMIT q->totalBytesChanged();
    }

    if (info.done)
        setFinished();

    if (downloadPaused != info.paused) {
        downloadPaused = info.paused;
        Q_EMIT q->isPausedChanged();
    }
}

void QWebEngineDownloadRequestPrivate::setFinished()
{
    if (downloadFinished)
        return;

    downloadFinished = true;
    Q_EMIT q_ptr->isFinishedChanged();
}

void QWebEngineDownloadRequestPrivate::answer()
{
    if (answered)
        return;

    if (profileAdapter) {
        const QString path = QDir(downloadDirectory).filePath(downloadFileName);
        bool accepted = downloadState != QWebEngineDownloadRequest::DownloadCancelled
                && downloadState != QWebEngineDownloadRequest::DownloadRequested;
        profileAdapter->acceptDownload(downloadId, accepted, useDownloadTargetCallback, path, savePageFormat);
        answered = true;
    }
}

/*!
    Accepts the current download request, which will start the download.

    If the item is in the \l DownloadRequested state, then it will transition
    into the \l DownloadInProgress state and the downloading will begin. If the
    item is in any other state, then nothing will happen.

    \sa isFinished, stateChanged()
*/

void QWebEngineDownloadRequest::accept()
{
    Q_D(QWebEngineDownloadRequest);

    if (d->downloadState != QWebEngineDownloadRequest::DownloadRequested)
        return;

    d->downloadState = QWebEngineDownloadRequest::DownloadInProgress;
    Q_EMIT stateChanged(d->downloadState);
    d->answer();
}

/*!
    Cancels the current download.

    If the item is in the \l DownloadInProgress state, then it will transition
    into the \l DownloadCancelled state, the downloading will stop, and partially
    downloaded files will be deleted from disk.

    If the item is in the \l DownloadCompleted state, then nothing will happen.
    If the item is in any other state, then it will transition into the \l
    DownloadCancelled state without further effect.

    \sa isFinished, stateChanged()
*/

void QWebEngineDownloadRequest::cancel()
{
    Q_D(QWebEngineDownloadRequest);

    QWebEngineDownloadRequest::DownloadState state = d->downloadState;

    if (state == QWebEngineDownloadRequest::DownloadCompleted)
        return;

    bool cancelled = state == QWebEngineDownloadRequest::DownloadCancelled;
    if (cancelled)
        return;

    // Check if the download manager has a DownloadItem for this ID
    // (network downloads or in progress page/resource saves)
    if (state == QWebEngineDownloadRequest::DownloadInProgress) {
        if (d->profileAdapter)
            cancelled = d->profileAdapter->cancelDownload(d->downloadId);
    }

    // Not cancelled downloads are not even started yet at this point
    if (!cancelled) {
        d->downloadState = QWebEngineDownloadRequest::DownloadCancelled;
        Q_EMIT stateChanged(d->downloadState);
        d->setFinished();
    }
    d->answer();
}

/*!
    Pauses the download.

    Has no effect if the state is not \l DownloadInProgress. Does not change the
    state.

    \sa resume(), isPaused()
*/

void QWebEngineDownloadRequest::pause()
{
    Q_D(QWebEngineDownloadRequest);

    QWebEngineDownloadRequest::DownloadState state = d->downloadState;

    if (state != QWebEngineDownloadRequest::DownloadInProgress)
        return;

    if (d->profileAdapter)
        d->profileAdapter->pauseDownload(d->downloadId);
}

/*!
    Resumes the current download if it was paused or interrupted.

    Has no effect if the state is not \l DownloadInProgress or \l
    DownloadInterrupted. Does not change the state.

    \sa pause(), isPaused(), state()
*/
void QWebEngineDownloadRequest::resume()
{
    Q_D(QWebEngineDownloadRequest);

    QWebEngineDownloadRequest::DownloadState state = d->downloadState;

    if (d->downloadFinished || (state != QWebEngineDownloadRequest::DownloadInProgress && state != QWebEngineDownloadRequest::DownloadInterrupted))
        return;
    if (d->profileAdapter)
        d->profileAdapter->resumeDownload(d->downloadId);
}

/*!
    \property QWebEngineDownloadRequest::id
    \brief The download item's ID.
*/

quint32 QWebEngineDownloadRequest::id() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadId;
}

/*!
    \fn void QWebEngineDownloadRequest::isPausedChanged()

    This signal is emitted whenever isPaused changes.

    \sa pause(), isPaused
*/

/*!
    \fn void QWebEngineDownloadRequest::stateChanged(DownloadState state)

    This signal is emitted whenever the download's \a state changes.

    \sa state(), DownloadState
*/

/*!
    \enum QWebEngineDownloadRequest::DownloadState

    This enum describes the state of the download:

    \value DownloadRequested Download has been requested, but has not been accepted yet.
    \value DownloadInProgress Download is in progress.
    \value DownloadCompleted Download completed successfully.
    \value DownloadCancelled Download has been cancelled.
    \value DownloadInterrupted Download has been interrupted (by the server or because of lost
            connectivity).
*/

/*!
    \enum QWebEngineDownloadRequest::SavePageFormat

    This enum describes the format that is used to save a web page.

    \value UnknownSaveFormat This is not a request for downloading a complete web page.
    \value SingleHtmlSaveFormat The page is saved as a single HTML page. Resources such as images
           are not saved.
    \value CompleteHtmlSaveFormat The page is saved as a complete HTML page, for example a directory
            containing the single HTML page and the resources.
    \value MimeHtmlSaveFormat The page is saved as a complete web page in the MIME HTML format.
*/

/*!
    \enum QWebEngineDownloadRequest::DownloadInterruptReason

    Describes the reason why a download was interrupted:

    \value NoReason Unknown reason or not interrupted.
    \value FileFailed General file operation failure.
    \value FileAccessDenied The file cannot be written locally, due to access restrictions.
    \value FileNoSpace Insufficient space on the target drive.
    \value FileNameTooLong The directory or file name is too long.
    \value FileTooLarge The file size exceeds the file system limitation.
    \value FileVirusInfected The file is infected with a virus.
    \value FileTransientError Temporary problem (for example the file is in use,
           out of memory, or too many files are opened at once).
    \value FileBlocked The file was blocked due to local policy.
    \value FileSecurityCheckFailed An attempt to check the safety of the download
           failed due to unexpected reasons.
    \value FileTooShort An attempt was made to seek past the end of a file when
           opening a file (as part of resuming a previously interrupted download).
    \value FileHashMismatch The partial file did not match the expected hash.

    \value NetworkFailed General network failure.
    \value NetworkTimeout The network operation has timed out.
    \value NetworkDisconnected The network connection has been terminated.
    \value NetworkServerDown The server has gone down.
    \value NetworkInvalidRequest The network request was invalid (for example, the
           original or redirected URL is invalid, has an unsupported scheme, or is disallowed by policy).

    \value ServerFailed General server failure.
    \value ServerBadContent The server does not have the requested data.
    \value ServerUnauthorized The server did not authorize access to the resource.
    \value ServerCertProblem A problem with the server certificate occurred.
    \value ServerForbidden Access forbidden by the server.
    \value ServerUnreachable Unexpected server response (might indicate that
           the responding server may not be the intended server).
    \value UserCanceled The user canceled the download.
*/

/*!
    \property QWebEngineDownloadRequest::state
    \brief The download item's current state.

    \sa DownloadState
*/

QWebEngineDownloadRequest::DownloadState QWebEngineDownloadRequest::state() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadState;
}

/*!
    \property QWebEngineDownloadRequest::totalBytes
    \brief The total amount of data to download in bytes.

    \c -1 means the size is unknown.
*/

qint64 QWebEngineDownloadRequest::totalBytes() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->totalBytes;
}

/*!
    \property QWebEngineDownloadRequest::receivedBytes
    \brief The amount of data in bytes that has been downloaded so far.

    \c -1 means the size is unknown.
*/

qint64 QWebEngineDownloadRequest::receivedBytes() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->receivedBytes;
}

/*!
    \property QWebEngineDownloadRequest::url
    \brief The download's origin URL.
*/

QUrl QWebEngineDownloadRequest::url() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadUrl;
}

/*!
    \property QWebEngineDownloadRequest::mimeType
    \brief The MIME type of the download.
*/

QString QWebEngineDownloadRequest::mimeType() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->mimeType;
}

/*!
    \property QWebEngineDownloadRequest::downloadDirectory
    \brief The download directory path.

    The download directory path can only be set in response to the
    QWebEngineProfile::downloadRequested() signal before the download is
    accepted. Past that point, this property has no effect on the download
    item's state.
*/

QString QWebEngineDownloadRequest::downloadDirectory() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadDirectory;
}

void QWebEngineDownloadRequest::setDownloadDirectory(const QString &directory)
{
    Q_D(QWebEngineDownloadRequest);
    if (d->downloadState != QWebEngineDownloadRequest::DownloadRequested) {
        qWarning("Setting the download directory is not allowed after the download has been accepted.");
        return;
    }

    if (!directory.isEmpty() && d->downloadDirectory != directory) {
        d->downloadDirectory = directory;
        Q_EMIT downloadDirectoryChanged();
    }

    if (!d->isCustomFileName && d->profileAdapter) {
        QString newFileName = QFileInfo(d->profileAdapter->determineDownloadPath(d->downloadDirectory,
                                                                                 d->suggestedFileName,
                                                                                 d->startTime)).fileName();
        if (d->downloadFileName != newFileName) {
            d->downloadFileName = std::move(newFileName);
            Q_EMIT downloadFileNameChanged();
        }
    }
}

/*!
    \property QWebEngineDownloadRequest::downloadFileName
    \brief The file name to download the file to.

    The download file name can only be set in response to the
    QWebEngineProfile::downloadRequested() signal before the download is
    accepted. Past that point, this property has no effect on the download
    item's state.
*/

QString QWebEngineDownloadRequest::downloadFileName() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadFileName;
}

void QWebEngineDownloadRequest::setDownloadFileName(const QString &fileName)
{
    Q_D(QWebEngineDownloadRequest);
    if (d->downloadState != QWebEngineDownloadRequest::DownloadRequested) {
        qWarning("Setting the download file name is not allowed after the download has been accepted.");
        return;
    }

    if (!fileName.isEmpty()) {
        d->downloadFileName = fileName;
        d->isCustomFileName = true;
        Q_EMIT downloadFileNameChanged();
    }
}

/*!
    \property QWebEngineDownloadRequest::suggestedFileName
    \brief The suggested file name.
*/

QString QWebEngineDownloadRequest::suggestedFileName() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->suggestedFileName;
}

/*!
    \property QWebEngineDownloadRequest::isFinished
    \brief Whether this download is finished (completed, cancelled,
           or non-resumable interrupted state).

    \sa state()
*/

bool QWebEngineDownloadRequest::isFinished() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadFinished;
}

/*!
    \property QWebEngineDownloadRequest::isPaused
    \brief Whether this download is paused.

    \sa pause(), resume()
*/

bool QWebEngineDownloadRequest::isPaused() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->downloadPaused;
}

/*!
    \property QWebEngineDownloadRequest::savePageFormat
    \brief The format the web page will be saved in if this is a download request for a web page.

    \sa isSavePageDownload
*/
QWebEngineDownloadRequest::SavePageFormat QWebEngineDownloadRequest::savePageFormat() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->savePageFormat;
}

void QWebEngineDownloadRequest::setSavePageFormat(QWebEngineDownloadRequest::SavePageFormat format)
{
  Q_D(QWebEngineDownloadRequest);
  if (d->savePageFormat != format) {
      d->savePageFormat = format;
      Q_EMIT savePageFormatChanged();
  }
}

/*!
    \property QWebEngineDownloadRequest::isSavePageDownload
    \brief Whether this is a download request for saving a web page.

    \sa savePageFormat
 */
bool QWebEngineDownloadRequest::isSavePageDownload() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->isSavePageDownload;
}

/*!
    \property QWebEngineDownloadRequest::interruptReason
    \brief The reason why the download was interrupted.

    \sa interruptReasonString
*/

QWebEngineDownloadRequest::DownloadInterruptReason QWebEngineDownloadRequest::interruptReason() const
{
    Q_D(const QWebEngineDownloadRequest);
    return d->interruptReason;
}

/*!
    \property QWebEngineDownloadRequest::interruptReasonString
    \brief A human-readable description of the reason for interrupting the download.

    \sa interruptReason
*/

QString QWebEngineDownloadRequest::interruptReasonString() const
{
    return ProfileAdapterClient::downloadInterruptReasonToString(
              static_cast<ProfileAdapterClient::DownloadInterruptReason>(interruptReason()));
}

/*!
    Returns the page the download was requested on. If the download was not triggered by content in a page,
    \c nullptr is returned.
*/
QWebEnginePage *QWebEngineDownloadRequest::page() const
{
    Q_D(const QWebEngineDownloadRequest);
    if (d->adapterClient && d->adapterClient->clientType() == QtWebEngineCore::WebContentsAdapterClient::WidgetsClient)
        return const_cast<QWebEnginePage *>(static_cast<const QWebEnginePage *>(d->adapterClient->holdingQObject()));
    return nullptr;
}


/*! \internal
*/
QWebEngineDownloadRequest::QWebEngineDownloadRequest(QWebEngineDownloadRequestPrivate *p, QObject *parent)
    : QObject(parent)
    , d_ptr(p)
{
    p->q_ptr = this;
}

/*! \internal
*/
QWebEngineDownloadRequest::~QWebEngineDownloadRequest()
{
    // MEMO Items are owned by profile by default and will be destroyed on profile's destruction
    //      It's not safe to access profile in that case, so we rely on profile to clean up items
    if (!isFinished())
        cancel();
}

QT_END_NAMESPACE

#include "moc_qwebenginedownloadrequest.cpp"
